En dybdegående guide til Reacts useSyncExternalStore-hook for problemfri integration med eksterne datakilder og state management-biblioteker. Lær at håndtere delt state effektivt i React-applikationer.
React useSyncExternalStore: Mestring af integration med ekstern state
Reacts useSyncExternalStore-hook, introduceret i React 18, giver en kraftfuld og effektiv måde at integrere eksterne datakilder og state management-biblioteker i dine React-komponenter. Denne hook giver komponenter mulighed for at abonnere på ændringer i eksterne stores, hvilket sikrer, at brugergrænsefladen altid afspejler de seneste data, samtidig med at ydeevnen optimeres. Denne guide giver en omfattende oversigt over useSyncExternalStore og dækker dens kernekoncepter, anvendelsesmønstre og best practices.
Forståelse af behovet for useSyncExternalStore
I mange React-applikationer vil du støde på scenarier, hvor state skal håndteres uden for komponenttræet. Dette er ofte tilfældet, når man arbejder med:
- Tredjepartsbiblioteker: Integration med biblioteker, der håndterer deres egen state (f.eks. en databaseforbindelse, et browser-API eller en fysikmotor).
- Delt state på tværs af komponenter: Håndtering af state, der skal deles mellem komponenter, som ikke er direkte relaterede (f.eks. brugerens autentificeringsstatus, applikationsindstillinger eller en global event bus).
- Eksterne datakilder: Hentning og visning af data fra eksterne API'er eller databaser.
Traditionelle state management-løsninger som useState og useReducer er velegnede til at håndtere lokal komponent-state. De er dog ikke designet til at håndtere ekstern state effektivt. At bruge dem direkte med eksterne datakilder kan føre til ydeevneproblemer, inkonsistente opdateringer og kompleks kode.
useSyncExternalStore adresserer disse udfordringer ved at tilbyde en standardiseret og optimeret måde at abonnere på ændringer i eksterne stores. Det sikrer, at komponenter kun gen-renderes, når de relevante data ændres, hvilket minimerer unødvendige opdateringer og forbedrer den overordnede ydeevne.
Kernekoncepter i useSyncExternalStore
useSyncExternalStore tager tre argumenter:
subscribe: En funktion, der tager en callback som argument og abonnerer på den eksterne store. Denne callback vil blive kaldt, hver gang storens data ændres.getSnapshot: En funktion, der returnerer et snapshot af dataene fra den eksterne store. Denne funktion skal returnere en stabil værdi, som React kan bruge til at afgøre, om dataene har ændret sig. Den skal være ren og hurtig.getServerSnapshot(valgfri): En funktion, der returnerer den oprindelige værdi af storen under server-side rendering. Dette er afgørende for at sikre, at den indledende HTML matcher client-side rendering. Den bruges KUN i server-side rendering-miljøer. Hvis den udelades i et client-side-miljø, bruger dengetSnapshoti stedet. Det er vigtigt, at denne værdi aldrig ændrer sig, efter den er blevet renderet på serversiden.
Her er en gennemgang af hvert argument:
1. subscribe
subscribe-funktionen er ansvarlig for at etablere en forbindelse mellem React-komponenten og den eksterne store. Den modtager en callback-funktion, som den skal kalde, hver gang storens data ændrer sig. Denne callback bruges typisk til at udløse en gen-rendering af komponenten.
Eksempel:
const subscribe = (callback) => {
store.addListener(callback);
return () => {
store.removeListener(callback);
};
};
I dette eksempel tilføjer store.addListener callback'en til storens liste af lyttere. Funktionen returnerer en oprydningsfunktion, der fjerner lytteren, når komponenten unmounts, hvilket forhindrer hukommelseslækager.
2. getSnapshot
getSnapshot-funktionen er ansvarlig for at hente et snapshot af dataene fra den eksterne store. Dette snapshot skal være en stabil værdi, som React kan bruge til at afgøre, om dataene har ændret sig. React bruger Object.is til at sammenligne det nuværende snapshot med det forrige. Derfor skal den være hurtig, og det anbefales kraftigt, at den returnerer en primitiv værdi (string, number, boolean, null eller undefined).
Eksempel:
const getSnapshot = () => {
return store.getData();
};
I dette eksempel returnerer store.getData de aktuelle data fra storen. React vil sammenligne denne værdi med den forrige værdi for at afgøre, om komponenten skal gen-renderes.
3. getServerSnapshot (Valgfri)
getServerSnapshot-funktionen er kun relevant, når der bruges server-side rendering (SSR). Denne funktion kaldes under den indledende server-rendering, og dens resultat bruges som den indledende værdi af storen, før hydrering sker på klienten. At returnere konsistente værdier er afgørende for succesfuld SSR.
Eksempel:
const getServerSnapshot = () => {
return store.getInitialDataForServer();
};
I dette eksempel returnerer `store.getInitialDataForServer` de indledende data, der er passende for server-side rendering.
Grundlæggende brugseksempel
Lad os se på et simpelt eksempel, hvor vi har en ekstern store, der håndterer en tæller. Vi kan bruge useSyncExternalStore til at vise tællerens værdi i en React-komponent:
// Ekstern store
const createStore = (initialValue) => {
let value = initialValue;
const listeners = new Set();
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
const getSnapshot = () => value;
const setState = (newValue) => {
value = newValue;
listeners.forEach((listener) => listener());
};
return {
subscribe,
getSnapshot,
setState,
};
};
const counterStore = createStore(0);
// React-komponent
import React from 'react';
import { useSyncExternalStore } from 'react';
function Counter() {
const count = useSyncExternalStore(counterStore.subscribe, counterStore.getSnapshot);
const increment = () => {
counterStore.setState(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
export default Counter;
I dette eksempel opretter createStore en simpel ekstern store, der håndterer en tællerværdi. Counter-komponenten bruger useSyncExternalStore til at abonnere på ændringer i storen og vise den aktuelle tællerstand. Når der klikkes på increment-knappen, opdaterer setState-funktionen storens værdi, hvilket udløser en gen-rendering af komponenten.
Integration med State Management-biblioteker
useSyncExternalStore er særligt nyttig til integration med state management-biblioteker som Zustand, Jotai og Recoil. Disse biblioteker tilbyder deres egne mekanismer til at håndtere state, og useSyncExternalStore giver dig mulighed for problemfrit at integrere dem i dine React-komponenter.
Her er et eksempel på integration med Zustand:
import { useStore } from 'zustand';
import { create } from 'zustand';
// Zustand store
const useBoundStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
// React-komponent
function Counter() {
const count = useStore(useBoundStore, (state) => state.count);
const increment = useStore(useBoundStore, (state) => state.increment);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
export default Counter;
Zustand forenkler oprettelsen af storen. Dets interne implementeringer af subscribe og getSnapshot bruges implicit, når du abonnerer på en bestemt state.
Her er et eksempel på integration med Jotai:
import { atom, useAtom } from 'jotai'
// Jotai atom
const countAtom = atom(0)
// React-komponent
function Counter() {
const [count, setCount] = useAtom(countAtom)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}
export default Counter;
Jotai bruger atomer til at håndtere state. useAtom håndterer internt abonnement og snapshotting.
Performanceoptimering
useSyncExternalStore tilbyder flere mekanismer til at optimere ydeevnen:
- Selektive opdateringer: React gen-renderer kun komponenten, når værdien returneret af
getSnapshotændres. Dette sikrer, at unødvendige gen-renderinger undgås. - Batching af opdateringer: React samler opdateringer fra flere eksterne stores i en enkelt gen-rendering. Dette reducerer antallet af gen-renderinger og forbedrer den samlede ydeevne.
- Undgåelse af "stale closures":
useSyncExternalStoresikrer, at komponenten altid har adgang til de seneste data fra den eksterne store, selv når der håndteres asynkrone opdateringer.
For yderligere at optimere ydeevnen, overvej følgende best practices:
- Minimer mængden af data returneret af
getSnapshot: Returner kun de data, som komponenten rent faktisk har brug for. Dette reducerer mængden af data, der skal sammenlignes, og forbedrer effektiviteten af opdateringsprocessen. - Brug memoization-teknikker: Memoize resultaterne af dyre beregninger eller datatransformationer. Dette kan forhindre unødvendige genberegninger og forbedre ydeevnen.
- Undgå unødvendige abonnementer: Abonner kun på den eksterne store, når komponenten rent faktisk er synlig. Dette kan reducere antallet af aktive abonnementer og forbedre den samlede ydeevne.
- Sørg for, at
getSnapshotkun returnerer et nyt *stabilt* objekt, hvis dataene har ændret sig: Undgå at oprette nye objekter/arrays/funktioner, hvis de underliggende data ikke rent faktisk har ændret sig. Returner det samme objekt via reference, hvis det er muligt.
Server-Side Rendering (SSR) med useSyncExternalStore
Når du bruger useSyncExternalStore med server-side rendering (SSR), er det afgørende at levere en getServerSnapshot-funktion. Denne funktion sikrer, at den indledende HTML, der renderes på serveren, matcher client-side rendering, hvilket forhindrer hydreringsfejl og forbedrer brugeroplevelsen.
Her er et eksempel på brug af getServerSnapshot:
const createStore = (initialValue) => {
let value = initialValue;
const listeners = new Set();
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
const getSnapshot = () => value;
const getServerSnapshot = () => initialValue; // Vigtigt for SSR
const setState = (newValue) => {
value = newValue;
listeners.forEach((listener) => listener());
};
return {
subscribe,
getSnapshot,
getServerSnapshot,
setState,
};
};
const counterStore = createStore(0);
// React-komponent
import React from 'react';
import { useSyncExternalStore } from 'react';
function Counter() {
const count = useSyncExternalStore(counterStore.subscribe, counterStore.getSnapshot, counterStore.getServerSnapshot);
const increment = () => {
counterStore.setState(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
export default Counter;
I dette eksempel returnerer getServerSnapshot den indledende værdi af tælleren. Dette sikrer, at den indledende HTML, der renderes på serveren, matcher client-side rendering. getServerSnapshot skal returnere en stabil og forudsigelig værdi. Den skal også udføre den samme logik som getSnapshot-funktionen på serveren. Undgå at tilgå browserspecifikke API'er eller globale variabler i getServerSnapshot.
Avancerede anvendelsesmønstre
useSyncExternalStore kan bruges i en række avancerede scenarier, herunder:
- Integration med browser-API'er: Abonnering på ændringer i browser-API'er som
localStorageellernavigator.onLine. - Oprettelse af custom hooks: Indkapsling af logikken for at abonnere på en ekstern store i en custom hook.
- Brug med Context API: Kombination af
useSyncExternalStoremed React Context API for at levere delt state til et komponenttræ.
Lad os se på et eksempel på at oprette en custom hook til at abonnere på localStorage:
import { useSyncExternalStore } from 'react';
function useLocalStorage(key, initialValue) {
const getSnapshot = () => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error("Error getting value from localStorage:", error);
return initialValue;
}
};
const subscribe = (callback) => {
window.addEventListener('storage', callback);
return () => window.removeEventListener('storage', callback);
};
const setItem = (value) => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
window.dispatchEvent(new Event('storage')); // Udløs manuelt storage-event for opdateringer på samme side
} catch (error) {
console.error("Error setting value in localStorage:", error);
}
};
const serverSnapshot = () => initialValue;
const storedValue = useSyncExternalStore(subscribe, getSnapshot, serverSnapshot);
return [storedValue, setItem];
}
export default useLocalStorage;
I dette eksempel er useLocalStorage en custom hook, der abonnerer på ændringer i localStorage. Den bruger useSyncExternalStore til at håndtere abonnementet og hente den aktuelle værdi fra localStorage. Den udsender også korrekt en storage-event for at sikre, at opdateringer på samme side afspejles (da `storage`-events kun udløses i andre faner). serverSnapshot sikrer, at de indledende værdier leveres korrekt i servermiljøer.
Best Practices og almindelige faldgruber
Her er nogle best practices og almindelige faldgruber, man skal undgå, når man bruger useSyncExternalStore:
- Undgå at mutere den eksterne store direkte: Brug altid storens API til at opdatere data. Direkte mutering af storen kan føre til inkonsistente opdateringer og uventet adfærd.
- Sørg for, at
getSnapshoter ren og hurtig:getSnapshotbør ikke have nogen sideeffekter og skal returnere en stabil værdi hurtigt. Dyre beregninger eller datatransformationer bør memoizes. - Lever en
getServerSnapshot-funktion ved brug af SSR: Dette er afgørende for at sikre, at den indledende HTML, der renderes på serveren, matcher client-side rendering. - Håndter fejl elegant: Brug try-catch-blokke til at håndtere potentielle fejl ved adgang til den eksterne store.
- Ryd op i abonnementer: Afmeld altid abonnementet på den eksterne store, når komponenten unmounts for at forhindre hukommelseslækager.
subscribe-funktionen skal returnere en oprydningsfunktion, der fjerner lytteren. - Forstå performance-konsekvenserne: Selvom
useSyncExternalStoreer optimeret for ydeevne, er det vigtigt at forstå den potentielle indvirkning af at abonnere på eksterne stores. Minimer mængden af data returneret afgetSnapshotog undgå unødvendige abonnementer. - Test grundigt: Sørg for, at integrationen med storen fungerer korrekt i forskellige scenarier, især ved server-side rendering og i concurrent mode.
Konklusion
useSyncExternalStore er en kraftfuld og effektiv hook til integration af eksterne datakilder og state management-biblioteker i dine React-komponenter. Ved at forstå dens kernekoncepter, anvendelsesmønstre og best practices kan du effektivt håndtere delt state i dine React-applikationer og optimere ydeevnen. Uanset om du integrerer med tredjepartsbiblioteker, håndterer delt state på tværs af komponenter eller henter data fra eksterne API'er, tilbyder useSyncExternalStore en standardiseret og pålidelig løsning. Brug den til at bygge mere robuste, vedligeholdelsesvenlige og performante React-applikationer for et globalt publikum.